跳到主要内容

数据结构 数组模拟队列与循环队列优化

队列介绍

队列是一个有序列表,可以用数组或是链表来实现。

遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出

示意图:使用数组模拟队列示意图

队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如上图,其中 maxSize 是该队列的最大容量。

因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front 及 rear 分别记录队列前后端的下标,front 会随着数据输出而改变,而 rear 则是随着数据输入而改变

入队列

当我们将数据存入队列时称为 addQueue,它的处理需要有两个步骤:

思路分析 将尾指针往后移:rear + 1

  • front == rear 时队列为空
  • 若尾指针 rear 小于队列的最大下标 maxSize - 1,则将数据存入 rear 所指的数组元素中,否则无法存入数据
  • rear == maxSize - 1 时队列满

出队列

从队列里面出队的操作称为 deQueue,它的处理也大同小异:

思路分析 将头指针往后移:front + 1

  • front == rear 时队列为空

单向队列

使用数组模拟队列

public class MyQueue {

private final int[] data;
private final int maxSize;

private int front;
private int rear;

public MyQueue(int size) {
this.maxSize = size;
this.data = new int[size];

this.front = -1;
this.rear = -1; // 因为每次插入都要先让 rear 移动一位再插入,所以最开始应该为 -1
}

//判断队列是否满
public boolean isFull() {
return rear == maxSize - 1;
}

//判断队列是否为空
public boolean isEmpty() {
return rear == front;
}

public boolean add(int e) {
if (isFull()) {
return false;
}
data[++rear] = e; // 尾指针后移一位后插入
return true;
}


public int remove() {
if (isEmpty()) {
throw new NoSuchElementException("队列为空,无法取得数据");
}
return data[++front]; //
}

public static void main(String[] args) {
MyQueue queue = new MyQueue(3);
queue.add(100);
queue.add(31);
System.out.println(queue.remove()); // 100
}
}

使用 List 集合模拟队列

使用 List 集合有个好处,就是无需考虑 rear 指针(因为 List 能自动扩容),只需要一个 front 指针表示当前头节点的位置

class MyQueue {
// 存储元素
private List<Integer> data;

// 记录起始位置的指针
private int front;

public MyQueue() {
data = new ArrayList<Integer>();
front = 0;
}

/** 在队列中插入一个元素,如果操作成功则返回 true */
public boolean addQueue(int x) {
data.add(x);
return true;
};

/** 从队列中删除元素,如果操作成功则返回 true
* 并不会真的删除元素,而是通过移动起始指针,来指示当前指向的第一个元素
*/
public boolean deQueue() {
if (isEmpty() == true) {
return false;
}
front++;
return true;
}

/** 从队列中获取最前面的项 */
public int Front() {
return data.get(front);
}

/** 检查队列是否为空,当指针的位置已经到和队列大小一样大了就表示元素已经删完了 */
public boolean isEmpty() {
return front >= data.size();
}
};

循环队列

但是上面这种 “单向队列” 的做法有个问题,就是并没有真正的删除,而是通过移动指针避开了这些删除的元素而已,随着起始指针的移动,浪费了越来越多的空间,当我们有空间限制时,这将是难以接受的。

对前面的数组模拟队列的优化,充分利用数组,因此将数组看做是一个环形的。(通过取模的方式来实现即可)

注意:不要小看这个循环队列,这里面还是有很多知识值得学习的,例如这里的取余机制,通过取余来获取一个不会大于 size 的索引,这个机制可以用来将索引置于头部

分析说明:

1、尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定,这个在做判断队列满的时候需要注意 (rear + 1) % maxSize == front 时表示满

2、rear == front 时为空

3、分析示意图:

思路如下:

1、front 变量的含义做一个调整:front 就指向队列的第一个元素,也就是说 data[front] 就是队列的第一个元素。

front 的初始值等于 0

2、rear 变量的含义做一个调整:rear 指向队列的最后一个元素的后一个位置,因为希望空出一个空间做为约定。

rear 的初始值等于 0

3、当队列满时,条件是 (rear + 1) % maxsize == front

4、对队列为空的条件,rear== front

5、当我们这样分析,队列中有效的数据的个数为 (rear + maxSize - front) % maxSize

6、我们就可以在原来的队列上修改得到一个环形队列

public class MyCircularQueue {

private final int[] data;
private final int maxSize;

private int front;
private int rear;

public MyCircularQueue(int size) {
this.maxSize = size;
this.data = new int[size];

this.front = 0;
this.rear = 0;
}

//判断队列是否满
public boolean isFull() {
return (rear + 1) % maxSize == front;
}

//判断队列是否为空
public boolean isEmpty() {
return rear == front;
}

public boolean add(int e) {
if (isFull()) {
return false;
}

//直接将数据加入
data[rear] = e;
//将rear后移
rear = (rear + 1) % maxSize;

return true;
}


public int remove() {
if (isEmpty()) {
throw new NoSuchElementException("队列为空,无法取得数据");
}

int value = data[front];
front = (front + 1) % maxSize;
return value;
}

}

Java 内置的队列

注意:实际上 LinkedList 实现的是双端队列 Deque 接口,只是 Deque 接口也继承了 Queue 接口而已

public class Main {
public static void main(String[] args) {
Queue<String> q = new LinkedList<>();
// 添加3个元素到队列:
q.offer("apple");
q.offer("pear");
q.offer("banana");
// 队首永远都是 apple,因为 peek() 不会删除它:
System.out.println(q.peek()); // apple
System.out.println(q.peek()); // apple
// 移除首元素并返回
System.out.println(q.poll()); // apple
}
}